Multi-Carrier¶
Hospital with CHP producing both electricity and heat.
This notebook introduces:
- Multiple energy carriers: Electricity, heat, and gas in one system
- CHP (Cogeneration): Equipment producing multiple outputs
- Electricity market: Buying and selling to the grid
- Carrier colors: Visual distinction between energy types
Setup¶
In [1]:
Copied!
import numpy as np
import pandas as pd
import plotly.express as px
import xarray as xr
import flixopt as fx
fx.CONFIG.notebook()
import numpy as np import pandas as pd import plotly.express as px import xarray as xr import flixopt as fx fx.CONFIG.notebook()
Out[1]:
flixopt.config.CONFIG
System Description¶
The hospital energy system:
Grid Buy ──►
[Electricity] ──► Hospital Elec. Load
Grid Sell ◄── ▲
│
Gas Grid ──► [Gas] ──► CHP ──────┘
│ │
│ ▼
│ [Heat] ──► Hospital Heat Load
│ ▲
└──► Boiler
Equipment:
- CHP: 200 kW electrical, ~250 kW thermal (η_el=40%, η_th=50%)
- Gas Boiler: 400 kW thermal backup
- Grid: Buy electricity at variable prices, sell at lower prices
Define Time Horizon and Demand Profiles¶
In [2]:
Copied!
# One week, hourly
timesteps = pd.date_range('2024-02-05', periods=168, freq='h')
hours = np.arange(168)
hour_of_day = hours % 24
# Hospital electricity demand (kW)
# Base load + daily pattern (higher during day for equipment, lighting)
elec_base = 150 # 24/7 critical systems
elec_daily = 100 * np.sin((hour_of_day - 6) * np.pi / 12) # Peak at noon
elec_daily = np.maximum(0, elec_daily)
electricity_demand = elec_base + elec_daily
# Hospital heat demand (kW)
# Higher in morning, drops during day, increases for hot water in evening
heat_pattern = np.select(
[
(hour_of_day >= 5) & (hour_of_day < 9), # Morning warmup
(hour_of_day >= 9) & (hour_of_day < 17), # Daytime
(hour_of_day >= 17) & (hour_of_day < 22), # Evening
],
[350, 250, 300],
default=200, # Night
)
heat_demand = heat_pattern.astype(float)
# Add random variation
np.random.seed(456)
electricity_demand += np.random.normal(0, 15, len(timesteps))
heat_demand += np.random.normal(0, 20, len(timesteps))
electricity_demand = np.clip(electricity_demand, 100, 300)
heat_demand = np.clip(heat_demand, 150, 400)
print(f'Electricity: {electricity_demand.min():.0f} - {electricity_demand.max():.0f} kW')
print(f'Heat: {heat_demand.min():.0f} - {heat_demand.max():.0f} kW')
# One week, hourly timesteps = pd.date_range('2024-02-05', periods=168, freq='h') hours = np.arange(168) hour_of_day = hours % 24 # Hospital electricity demand (kW) # Base load + daily pattern (higher during day for equipment, lighting) elec_base = 150 # 24/7 critical systems elec_daily = 100 * np.sin((hour_of_day - 6) * np.pi / 12) # Peak at noon elec_daily = np.maximum(0, elec_daily) electricity_demand = elec_base + elec_daily # Hospital heat demand (kW) # Higher in morning, drops during day, increases for hot water in evening heat_pattern = np.select( [ (hour_of_day >= 5) & (hour_of_day < 9), # Morning warmup (hour_of_day >= 9) & (hour_of_day < 17), # Daytime (hour_of_day >= 17) & (hour_of_day < 22), # Evening ], [350, 250, 300], default=200, # Night ) heat_demand = heat_pattern.astype(float) # Add random variation np.random.seed(456) electricity_demand += np.random.normal(0, 15, len(timesteps)) heat_demand += np.random.normal(0, 20, len(timesteps)) electricity_demand = np.clip(electricity_demand, 100, 300) heat_demand = np.clip(heat_demand, 150, 400) print(f'Electricity: {electricity_demand.min():.0f} - {electricity_demand.max():.0f} kW') print(f'Heat: {heat_demand.min():.0f} - {heat_demand.max():.0f} kW')
Electricity: 106 - 272 kW Heat: 150 - 397 kW
In [3]:
Copied!
# Electricity prices (€/kWh)
# Time-of-use: expensive during day, cheaper at night
elec_buy_price = np.where(
(hour_of_day >= 7) & (hour_of_day <= 21),
0.35, # Peak - high electricity prices make CHP attractive
0.20, # Off-peak
)
# Feed-in tariff (sell price) - allows selling excess CHP electricity
elec_sell_price = 0.12 # Fixed feed-in rate
# Gas price - relatively low, favoring gas-based generation
gas_price = 0.05 # €/kWh
# Electricity prices (€/kWh) # Time-of-use: expensive during day, cheaper at night elec_buy_price = np.where( (hour_of_day >= 7) & (hour_of_day <= 21), 0.35, # Peak - high electricity prices make CHP attractive 0.20, # Off-peak ) # Feed-in tariff (sell price) - allows selling excess CHP electricity elec_sell_price = 0.12 # Fixed feed-in rate # Gas price - relatively low, favoring gas-based generation gas_price = 0.05 # €/kWh
In [4]:
Copied!
# Visualize demands and prices with plotly - using xarray and faceting
profiles = xr.Dataset(
{
'Electricity Demand [kW]': xr.DataArray(electricity_demand, dims=['time'], coords={'time': timesteps}),
'Heat Demand [kW]': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}),
'Elec. Buy Price [€/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}),
}
)
df = profiles.to_dataframe().reset_index().melt(id_vars='time', var_name='variable', value_name='value')
fig = px.line(df, x='time', y='value', facet_col='variable', height=300)
fig.update_yaxes(matches=None, showticklabels=True)
fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1]))
fig
# Visualize demands and prices with plotly - using xarray and faceting profiles = xr.Dataset( { 'Electricity Demand [kW]': xr.DataArray(electricity_demand, dims=['time'], coords={'time': timesteps}), 'Heat Demand [kW]': xr.DataArray(heat_demand, dims=['time'], coords={'time': timesteps}), 'Elec. Buy Price [€/kWh]': xr.DataArray(elec_buy_price, dims=['time'], coords={'time': timesteps}), } ) df = profiles.to_dataframe().reset_index().melt(id_vars='time', var_name='variable', value_name='value') fig = px.line(df, x='time', y='value', facet_col='variable', height=300) fig.update_yaxes(matches=None, showticklabels=True) fig.for_each_annotation(lambda a: a.update(text=a.text.split('=')[-1])) fig
Build the Multi-Carrier System¶
In [5]:
Copied!
flow_system = fx.FlowSystem(timesteps)
flow_system.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
flow_system.add_elements(
# === Buses with carriers for visual distinction ===
fx.Bus('Electricity', carrier='electricity'),
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
# === Effects ===
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
fx.Effect('CO2', 'kg', 'CO2 Emissions'), # Track emissions too
# === Gas Supply ===
fx.Source(
'GasGrid',
outputs=[
fx.Flow(
'Gas',
bus='Gas',
size=1000,
effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2}, # Gas: 0.2 kg CO2/kWh
)
],
),
# === Electricity Grid (buy) ===
fx.Source(
'GridBuy',
outputs=[
fx.Flow(
'Electricity',
bus='Electricity',
size=500,
effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}, # Grid: 0.4 kg CO2/kWh
)
],
),
# === Electricity Grid (sell) - negative cost = revenue ===
fx.Sink(
'GridSell',
inputs=[
fx.Flow(
'Electricity',
bus='Electricity',
size=200,
effects_per_flow_hour={'costs': -elec_sell_price}, # Negative = income
)
],
),
# === CHP Unit (Combined Heat and Power) ===
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.40, # 40% to electricity
thermal_efficiency=0.50, # 50% to heat (total: 90%)
status_parameters=fx.StatusParameters(
effects_per_startup={'costs': 30},
min_uptime=3,
),
electrical_flow=fx.Flow('P_el', bus='Electricity', size=200),
thermal_flow=fx.Flow('Q_th', bus='Heat', size=250),
fuel_flow=fx.Flow(
'Q_fuel',
bus='Gas',
size=500,
relative_minimum=0.4, # Min 40% load
),
),
# === Gas Boiler (heat only) ===
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=400),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
# === Hospital Loads ===
fx.Sink(
'HospitalElec',
inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)],
),
fx.Sink(
'HospitalHeat',
inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)],
),
)
flow_system = fx.FlowSystem(timesteps) flow_system.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) flow_system.add_elements( # === Buses with carriers for visual distinction === fx.Bus('Electricity', carrier='electricity'), fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), # === Effects === fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), fx.Effect('CO2', 'kg', 'CO2 Emissions'), # Track emissions too # === Gas Supply === fx.Source( 'GasGrid', outputs=[ fx.Flow( 'Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2}, # Gas: 0.2 kg CO2/kWh ) ], ), # === Electricity Grid (buy) === fx.Source( 'GridBuy', outputs=[ fx.Flow( 'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}, # Grid: 0.4 kg CO2/kWh ) ], ), # === Electricity Grid (sell) - negative cost = revenue === fx.Sink( 'GridSell', inputs=[ fx.Flow( 'Electricity', bus='Electricity', size=200, effects_per_flow_hour={'costs': -elec_sell_price}, # Negative = income ) ], ), # === CHP Unit (Combined Heat and Power) === fx.linear_converters.CHP( 'CHP', electrical_efficiency=0.40, # 40% to electricity thermal_efficiency=0.50, # 50% to heat (total: 90%) status_parameters=fx.StatusParameters( effects_per_startup={'costs': 30}, min_uptime=3, ), electrical_flow=fx.Flow('P_el', bus='Electricity', size=200), thermal_flow=fx.Flow('Q_th', bus='Heat', size=250), fuel_flow=fx.Flow( 'Q_fuel', bus='Gas', size=500, relative_minimum=0.4, # Min 40% load ), ), # === Gas Boiler (heat only) === fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Q_th', bus='Heat', size=400), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), # === Hospital Loads === fx.Sink( 'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)], ), fx.Sink( 'HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)], ), )
Run Optimization¶
In [6]:
Copied!
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
flow_system.optimize(fx.solvers.HighsSolver(mip_gap=0.01));
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms
MIP linopy-problem-kel8ei02 has 4894 rows; 4391 cols; 15143 nonzeros; 1008 integer variables (1008 binary)
Coefficient ranges:
Matrix [1e-05, 5e+02]
Cost [1e+00, 1e+00]
Bound [1e+00, 1e+03]
RHS [1e-05, 2e+02]
WARNING: Problem has some excessively small row bounds
Presolving model
2631 rows, 1741 cols, 6717 nonzeros 0s
2407 rows, 1498 cols, 6424 nonzeros 0s
Presolve reductions: rows 2407(-2487); columns 1498(-2893); nonzeros 6424(-8719)
Solving MIP model with:
2407 rows
1498 cols (1006 binary, 0 integer, 0 implied int., 492 continuous, 0 domain fixed)
6424 nonzeros
Src: B => Branching; C => Central rounding; F => Feasibility pump; H => Heuristic;
I => Shifting; J => Feasibility jump; L => Sub-MIP; P => Empty MIP; R => Randomized rounding;
S => Solve LP; T => Evaluate node; U => Unbounded; X => User solution; Y => HiGHS solution;
Z => ZI Round; l => Trivial lower; p => Trivial point; u => Trivial upper; z => Trivial zero
Nodes | B&B Tree | Objective Bounds | Dynamic Constraints | Work
Src Proc. InQueue | Leaves Expl. | BestBound BestSol Gap | Cuts InLp Confl. | LpIters Time
J 0 0 0 0.00% -inf 11586.321745 Large 0 0 0 0 0.1s
T 0 0 0 0.00% 2376.304875 4674.296833 49.16% 0 0 0 951 0.1s
1 0 1 100.00% 4674.296833 4674.296833 0.00% 0 0 0 951 0.1s
Solving report
Model linopy-problem-kel8ei02
Status Optimal
Primal bound 4674.29683258
Dual bound 4674.29683258
Gap 0% (tolerance: 1%)
P-D integral 0.0123991318763
Solution status feasible
4674.29683258 (objective)
0 (bound viol.)
2.22044604925e-16 (int. viol.)
0 (row viol.)
Timing 0.07
Max sub-MIP depth 0
Nodes 1
Repair LPs 0
LP iterations 951
0 (strong br.)
0 (separation)
0 (heuristics)
In [7]:
Copied!
flow_system.statistics.plot.balance('Electricity')
flow_system.statistics.plot.balance('Electricity')
Out[7]:
Heat Balance¶
In [8]:
Copied!
flow_system.statistics.plot.balance('Heat')
flow_system.statistics.plot.balance('Heat')
Out[8]:
Gas Balance¶
In [9]:
Copied!
flow_system.statistics.plot.balance('Gas')
flow_system.statistics.plot.balance('Gas')
Out[9]:
CHP Operation Pattern¶
In [10]:
Copied!
flow_system.statistics.plot.heatmap('CHP(P_el)')
flow_system.statistics.plot.heatmap('CHP(P_el)')
Out[10]:
Cost and Emissions Summary¶
In [11]:
Copied!
total_costs = flow_system.solution['costs'].item()
total_co2 = flow_system.solution['CO2'].item()
# Energy flows
flow_rates = flow_system.statistics.flow_rates
grid_buy = flow_rates['GridBuy(Electricity)'].sum().item()
grid_sell = flow_rates['GridSell(Electricity)'].sum().item()
chp_elec = flow_rates['CHP(P_el)'].sum().item()
chp_heat = flow_rates['CHP(Q_th)'].sum().item()
boiler_heat = flow_rates['Boiler(Q_th)'].sum().item()
total_elec = electricity_demand.sum()
total_heat = heat_demand.sum()
print('=== Energy Summary ===')
print(f'Total electricity demand: {total_elec:.0f} kWh')
print(f' - From CHP: {chp_elec:.0f} kWh ({chp_elec / total_elec * 100:.1f}%)')
print(f' - From Grid: {grid_buy:.0f} kWh ({grid_buy / total_elec * 100:.1f}%)')
print(f' - Sold to Grid: {grid_sell:.0f} kWh')
print()
print(f'Total heat demand: {total_heat:.0f} kWh')
print(f' - From CHP: {chp_heat:.0f} kWh ({chp_heat / total_heat * 100:.1f}%)')
print(f' - From Boiler: {boiler_heat:.0f} kWh ({boiler_heat / total_heat * 100:.1f}%)')
print()
print('=== Costs & Emissions ===')
print(f'Total costs: {total_costs:.2f} €')
print(f'Total CO2: {total_co2:.0f} kg')
print(f'Specific costs: {total_costs / (total_elec + total_heat) * 100:.2f} ct/kWh')
print(f'Specific CO2: {total_co2 / (total_elec + total_heat) * 1000:.1f} g/kWh')
total_costs = flow_system.solution['costs'].item() total_co2 = flow_system.solution['CO2'].item() # Energy flows flow_rates = flow_system.statistics.flow_rates grid_buy = flow_rates['GridBuy(Electricity)'].sum().item() grid_sell = flow_rates['GridSell(Electricity)'].sum().item() chp_elec = flow_rates['CHP(P_el)'].sum().item() chp_heat = flow_rates['CHP(Q_th)'].sum().item() boiler_heat = flow_rates['Boiler(Q_th)'].sum().item() total_elec = electricity_demand.sum() total_heat = heat_demand.sum() print('=== Energy Summary ===') print(f'Total electricity demand: {total_elec:.0f} kWh') print(f' - From CHP: {chp_elec:.0f} kWh ({chp_elec / total_elec * 100:.1f}%)') print(f' - From Grid: {grid_buy:.0f} kWh ({grid_buy / total_elec * 100:.1f}%)') print(f' - Sold to Grid: {grid_sell:.0f} kWh') print() print(f'Total heat demand: {total_heat:.0f} kWh') print(f' - From CHP: {chp_heat:.0f} kWh ({chp_heat / total_heat * 100:.1f}%)') print(f' - From Boiler: {boiler_heat:.0f} kWh ({boiler_heat / total_heat * 100:.1f}%)') print() print('=== Costs & Emissions ===') print(f'Total costs: {total_costs:.2f} €') print(f'Total CO2: {total_co2:.0f} kg') print(f'Specific costs: {total_costs / (total_elec + total_heat) * 100:.2f} ct/kWh') print(f'Specific CO2: {total_co2 / (total_elec + total_heat) * 1000:.1f} g/kWh')
=== Energy Summary === Total electricity demand: 30671 kWh - From CHP: 31304 kWh (102.1%) - From Grid: 2475 kWh (8.1%) - Sold to Grid: 3108 kWh Total heat demand: 44175 kWh - From CHP: 39131 kWh (88.6%) - From Boiler: 5045 kWh (11.4%) === Costs & Emissions === Total costs: 4674.30 € Total CO2: 17739 kg Specific costs: 6.25 ct/kWh Specific CO2: 237.0 g/kWh
Compare: What if No CHP?¶
How much does the CHP save compared to buying all electricity?
In [12]:
Copied!
# Build system without CHP
fs_no_chp = fx.FlowSystem(timesteps)
fs_no_chp.add_carriers(
fx.Carrier('gas', '#3498db', 'kW'),
fx.Carrier('electricity', '#f1c40f', 'kW'),
fx.Carrier('heat', '#e74c3c', 'kW'),
)
fs_no_chp.add_elements(
fx.Bus('Electricity', carrier='electricity'),
fx.Bus('Heat', carrier='heat'),
fx.Bus('Gas', carrier='gas'),
fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True),
fx.Effect('CO2', 'kg', 'CO2 Emissions'),
fx.Source(
'GasGrid',
outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2})],
),
fx.Source(
'GridBuy',
outputs=[
fx.Flow(
'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4}
)
],
),
# Only boiler for heat
fx.linear_converters.Boiler(
'Boiler',
thermal_efficiency=0.92,
thermal_flow=fx.Flow('Q_th', bus='Heat', size=500),
fuel_flow=fx.Flow('Q_fuel', bus='Gas'),
),
fx.Sink(
'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)]
),
fx.Sink('HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)]),
)
fs_no_chp.optimize(fx.solvers.HighsSolver())
no_chp_costs = fs_no_chp.solution['costs'].item()
no_chp_co2 = fs_no_chp.solution['CO2'].item()
print('=== CHP Benefit Analysis ===')
print(f'Without CHP: {no_chp_costs:.2f} € / {no_chp_co2:.0f} kg CO2')
print(f'With CHP: {total_costs:.2f} € / {total_co2:.0f} kg CO2')
print(f'Cost savings: {no_chp_costs - total_costs:.2f} € ({(no_chp_costs - total_costs) / no_chp_costs * 100:.1f}%)')
print(f'CO2 reduction: {no_chp_co2 - total_co2:.0f} kg ({(no_chp_co2 - total_co2) / no_chp_co2 * 100:.1f}%)')
# Build system without CHP fs_no_chp = fx.FlowSystem(timesteps) fs_no_chp.add_carriers( fx.Carrier('gas', '#3498db', 'kW'), fx.Carrier('electricity', '#f1c40f', 'kW'), fx.Carrier('heat', '#e74c3c', 'kW'), ) fs_no_chp.add_elements( fx.Bus('Electricity', carrier='electricity'), fx.Bus('Heat', carrier='heat'), fx.Bus('Gas', carrier='gas'), fx.Effect('costs', '€', 'Total Costs', is_standard=True, is_objective=True), fx.Effect('CO2', 'kg', 'CO2 Emissions'), fx.Source( 'GasGrid', outputs=[fx.Flow('Gas', bus='Gas', size=1000, effects_per_flow_hour={'costs': gas_price, 'CO2': 0.2})], ), fx.Source( 'GridBuy', outputs=[ fx.Flow( 'Electricity', bus='Electricity', size=500, effects_per_flow_hour={'costs': elec_buy_price, 'CO2': 0.4} ) ], ), # Only boiler for heat fx.linear_converters.Boiler( 'Boiler', thermal_efficiency=0.92, thermal_flow=fx.Flow('Q_th', bus='Heat', size=500), fuel_flow=fx.Flow('Q_fuel', bus='Gas'), ), fx.Sink( 'HospitalElec', inputs=[fx.Flow('Load', bus='Electricity', size=1, fixed_relative_profile=electricity_demand)] ), fx.Sink('HospitalHeat', inputs=[fx.Flow('Load', bus='Heat', size=1, fixed_relative_profile=heat_demand)]), ) fs_no_chp.optimize(fx.solvers.HighsSolver()) no_chp_costs = fs_no_chp.solution['costs'].item() no_chp_co2 = fs_no_chp.solution['CO2'].item() print('=== CHP Benefit Analysis ===') print(f'Without CHP: {no_chp_costs:.2f} € / {no_chp_co2:.0f} kg CO2') print(f'With CHP: {total_costs:.2f} € / {total_co2:.0f} kg CO2') print(f'Cost savings: {no_chp_costs - total_costs:.2f} € ({(no_chp_costs - total_costs) / no_chp_costs * 100:.1f}%)') print(f'CO2 reduction: {no_chp_co2 - total_co2:.0f} kg ({(no_chp_co2 - total_co2) / no_chp_co2 * 100:.1f}%)')
Running HiGHS 1.12.0 (git hash: 755a8e0): Copyright (c) 2025 HiGHS under MIT licence terms LP linopy-problem-093ehxv0 has 1863 rows; 2199 cols; 5397 nonzeros Coefficient ranges: Matrix [5e-02, 1e+00] Cost [1e+00, 1e+00] Bound [1e+02, 1e+03] RHS [0e+00, 0e+00] Presolving model 0 rows, 0 cols, 0 nonzeros 0s 0 rows, 0 cols, 0 nonzeros 0s Presolve reductions: rows 0(-1863); columns 0(-2199); nonzeros 0(-5397) - Reduced to empty Performed postsolve Solving the original LP from the solution after postsolve Model name : linopy-problem-093ehxv0 Model status : Optimal Objective value : 1.1703962760e+04 P-D objective error : 5.4393465493e-16 HiGHS run time : 0.00 === CHP Benefit Analysis === Without CHP: 11703.96 € / 21872 kg CO2 With CHP: 4674.30 € / 17739 kg CO2 Cost savings: 7029.67 € (60.1%) CO2 reduction: 4133 kg (18.9%)
Energy Flow Sankey¶
A Sankey diagram visualizes the total energy flows through the multi-carrier system:
In [13]:
Copied!
flow_system.statistics.plot.sankey.flows()
flow_system.statistics.plot.sankey.flows()
Out[13]:
Key Concepts¶
Multi-Carrier Systems¶
- Multiple buses for different energy carriers (electricity, heat, gas)
- Components can connect to multiple buses (CHP produces both electricity and heat)
- Carriers enable automatic coloring in visualizations
CHP Modeling¶
fx.linear_converters.CHP(
'CHP',
electrical_efficiency=0.40, # Fuel → Electricity
thermal_efficiency=0.50, # Fuel → Heat
# Total efficiency = 0.40 + 0.50 = 0.90 (90%)
electrical_flow=fx.Flow('P_el', bus='Electricity', size=200),
thermal_flow=fx.Flow('Q_th', bus='Heat', size=250),
fuel_flow=fx.Flow('Q_fuel', bus='Gas', size=500),
)
Electricity Markets¶
- Buy: Source with positive cost
- Sell: Sink with negative cost (= revenue)
- Different prices for buy vs. sell (spread)
Tracking Multiple Effects¶
fx.Effect('costs', '€', 'Total Costs', is_objective=True) # Minimize this
fx.Effect('CO2', 'kg', 'CO2 Emissions') # Just track, don't optimize
Summary¶
You learned how to:
- Model multiple energy carriers (electricity, heat, gas)
- Use CHP for combined heat and power production
- Model electricity markets with buy/sell prices
- Track multiple effects (costs and emissions)
- Analyze multi-carrier balances
Next Steps¶
- 06a-time-varying-parameters: Variable efficiency based on conditions
- 07-scenarios-and-periods: Plan under uncertainty